#include "HttpSession.hpp"

static void
fail(boost::beast::error_code ec, char const *what)
{
    std::cerr << what << ": " << ec.message() << "\n";
}

// Return a reasonable mime type based on the extension of a file.
static boost::beast::string_view
mime_type(boost::beast::string_view path)
{
    using boost::beast::iequals;
    auto const ext = [&path]
    {
        auto const pos = path.rfind(".");
        if (pos == boost::beast::string_view::npos)
            return boost::beast::string_view{};
        return path.substr(pos);
    }();
    if (iequals(ext, ".htm"))
        return "text/html";
    if (iequals(ext, ".html"))
        return "text/html";
    if (iequals(ext, ".php"))
        return "text/html";
    if (iequals(ext, ".css"))
        return "text/css";
    if (iequals(ext, ".txt"))
        return "text/plain";
    if (iequals(ext, ".js"))
        return "application/javascript";
    if (iequals(ext, ".json"))
        return "application/json";
    if (iequals(ext, ".xml"))
        return "application/xml";
    if (iequals(ext, ".swf"))
        return "application/x-shockwave-flash";
    if (iequals(ext, ".flv"))
        return "video/x-flv";
    if (iequals(ext, ".png"))
        return "image/png";
    if (iequals(ext, ".jpe"))
        return "image/jpeg";
    if (iequals(ext, ".jpeg"))
        return "image/jpeg";
    if (iequals(ext, ".jpg"))
        return "image/jpeg";
    if (iequals(ext, ".gif"))
        return "image/gif";
    if (iequals(ext, ".bmp"))
        return "image/bmp";
    if (iequals(ext, ".ico"))
        return "image/vnd.microsoft.icon";
    if (iequals(ext, ".tiff"))
        return "image/tiff";
    if (iequals(ext, ".tif"))
        return "image/tiff";
    if (iequals(ext, ".svg"))
        return "image/svg+xml";
    if (iequals(ext, ".svgz"))
        return "image/svg+xml";
    return "application/text";
}

// Append an HTTP rel-path to a local filesystem path.
// The returned path is normalized for the platform.
static std::string
path_cat(
    boost::beast::string_view base,
    boost::beast::string_view path)
{
    if (base.empty())
        return std::string(path);
    std::string result(base);
#ifdef BOOST_MSVC
    char constexpr path_separator = '\\';
    if (result.back() == path_separator)
        result.resize(result.size() - 1);
    result.append(path.data(), path.size());
    for (auto &c : result)
        if (c == '/')
            c = path_separator;
#else
    char constexpr path_separator = '/';
    if (result.back() == path_separator)
        result.resize(result.size() - 1);
    result.append(path.data(), path.size());
#endif
    return result;
}

std::string HttpSession::server_name = BOOST_BEAST_VERSION_STRING;

HttpSession::
    HttpSession(boost::asio::ip::tcp::socket &&socket,
                std::string const &doc_root,
                std::map<std::string, RequestHandler> &get_handlers,
                std::map<std::string, RequestHandler> &post_handlers)
    : stream(std::move(socket)), get_handlers(get_handlers), post_handlers(post_handlers), doc_root(doc_root)
{
    // ...
}

HttpSession::~HttpSession()
{
}

void HttpSession::start()
{
    run();
}

// Start the asynchronous operation
void HttpSession::run()
{
    // We need to be executing within a strand to perform async operations
    // on the I/O objects in this HttpSession. Although not strictly necessary
    // for single-threaded contexts, this example code is written to be
    // thread-safe by default.
    boost::asio::dispatch(stream.get_executor(),
                  boost::beast::bind_front_handler(
                      &HttpSession::do_read,
                      shared_from_this()));
}

void HttpSession::do_read()
{
    // Make the request empty before reading,
    // otherwise the operation behavior is undefined.
    request = {};

    // Set the timeout.
    stream.expires_after(std::chrono::seconds(30));

    // Read a request
    boost::beast::http::async_read(stream, buffer, request,
                     boost::beast::bind_front_handler(
                         &HttpSession::on_read,
                         shared_from_this()));
}

void HttpSession::on_read(
    boost::beast::error_code ec,
    std::size_t bytes_transferred)
{
    boost::ignore_unused(bytes_transferred);

    // This means they closed the connection
    if (ec == boost::beast::http::error::end_of_stream)
        return do_close();

    if (ec)
        return fail(ec, "read");

    // Send the response
    send_response(handle_request());
}

void HttpSession::send_response(boost::beast::http::message_generator &&msg)
{
    bool keep_alive = msg.keep_alive();

    // Write the response
    boost::beast::async_write(
        stream,
        std::move(msg),
        boost::beast::bind_front_handler(
            &HttpSession::on_write, shared_from_this(), keep_alive));
}

void HttpSession::on_write(
    bool keep_alive,
    boost::beast::error_code ec,
    std::size_t bytes_transferred)
{
    boost::ignore_unused(bytes_transferred);

    if (ec)
        return fail(ec, "write");

    if (!keep_alive)
    {
        // This means we should close the connection, usually because
        // the response indicated the "Connection: close" semantic.
        return do_close();
    }

    // Read another request
    do_read();
}

void HttpSession::do_close()
{
    // Send a TCP shutdown
    boost::beast::error_code ec;
    stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);

    // At this point the connection is closed gracefully
}

// Return a response for the given request.
//
// The concrete type of the response message (which depends on the
// request), is type-erased in message_generator.
boost::beast::http::message_generator
HttpSession::handle_request()
{
    // Returns a bad request response
    auto const bad_request =
        [this](boost::beast::string_view why)
    {
        boost::beast::http::response<boost::beast::http::string_body> res{boost::beast::http::status::bad_request, request.version()};
        res.set(boost::beast::http::field::server, server_name);
        res.set(boost::beast::http::field::content_type, "text/html");
        res.keep_alive(request.keep_alive());
        res.body() = std::string(why);
        res.prepare_payload();
        return res;
    };

    // Returns a not found response
    auto const not_found =
        [this](boost::beast::string_view target)
    {
        boost::beast::http::response<boost::beast::http::string_body> res{boost::beast::http::status::not_found, request.version()};
        res.set(boost::beast::http::field::server, server_name);
        res.set(boost::beast::http::field::content_type, "text/html");
        res.keep_alive(request.keep_alive());
        res.body() = "The resource '" + std::string(target) + "' was not found.";
        res.prepare_payload();
        return res;
    };

    // Returns a server error response
    auto const server_error =
        [this](boost::beast::string_view what)
    {
        boost::beast::http::response<boost::beast::http::string_body> res{boost::beast::http::status::internal_server_error, request.version()};
        res.set(boost::beast::http::field::server, server_name);
        res.set(boost::beast::http::field::content_type, "text/html");
        res.keep_alive(request.keep_alive());
        res.body() = "An error occurred: '" + std::string(what) + "'";
        res.prepare_payload();
        return res;
    };

    auto const httpResponse = [this](HttpResponse &response)
    {
        boost::beast::http::response<boost::beast::http::string_body> res{response.result(), response.version()};
        res.base() = response.base();
        res.body() = response.body();
        return res;
    };
    HttpResponse response;
    response.result(HttpResponse::status::ok);
    response.version(request.version());
    response.set(boost::beast::http::field::server, server_name);

    boost::beast::http::response<boost::beast::http::string_body> res1(response);

    std::cout << request  << std::endl;
    std::cout << "***********************************************************\n\n" << std::endl;

    // Make sure we can handle the method
    switch (request.method())
    {
    case boost::beast::http::verb::get:
    {
        request.parse_url();
        auto it = get_handlers.find(request.path());

        if (it != get_handlers.end())
        {
            it->second(request, response);
            boost::beast::http::response<boost::beast::http::string_body> res(response);
            res.prepare_payload();
            return res;
        }
    }
    break;

    case boost::beast::http::verb::post:
    {
        auto it = post_handlers.find(request.target());
        if (it != post_handlers.end())
        {
            it->second(request, response);
            response.prepare_payload();
            return boost::beast::http::response<boost::beast::http::string_body>(response);
        }
    }
    break;

    case boost::beast::http::verb::head:
    {
    }
    break;

    default:
    {
        return bad_request("Unknown HTTP-method");
    }
    break;
    }

    // Request path must be absolute and not contain "..".
    if (request.target().empty() ||
        request.target()[0] != '/' ||
        request.target().find("..") != boost::beast::string_view::npos)
        return bad_request("Illegal request-target");

    // Build the path to the requested file
    std::string path = path_cat(doc_root, request.target());
    if (request.target().back() == '/')
        path.append("index.html");

    // Attempt to open the file
    boost::beast::error_code ec;
    boost::beast::http::file_body::value_type body;
    body.open(path.c_str(), boost::beast::file_mode::scan, ec);

    // Handle the case where the file doesn't exist
    if (ec == boost::beast::errc::no_such_file_or_directory)
        return not_found(request.target());

    // Handle an unknown error
    if (ec)
        return server_error(ec.message());

    // Cache the size since we need it after the move
    auto const size = body.size();

    // Respond to HEAD request
    if (request.method() == boost::beast::http::verb::head)
    {
        boost::beast::http::response<boost::beast::http::string_body> res{boost::beast::http::status::ok, request.version()};
        res.set(boost::beast::http::field::server, server_name);
        res.set(boost::beast::http::field::content_type, mime_type(path));
        res.content_length(size);
        res.keep_alive(request.keep_alive());
        return res;
    }

    // Respond to GET request
    boost::beast::http::response<boost::beast::http::file_body> res{
        std::piecewise_construct,
        std::make_tuple(std::move(body)),
        std::make_tuple(boost::beast::http::status::ok, request.version())};
    res.set(boost::beast::http::field::server, server_name);
    res.set(boost::beast::http::field::content_type, mime_type(path));
    res.content_length(size);
    res.keep_alive(request.keep_alive());
    return res;
}
